/*
**  ClanLib SDK
**  Copyright (c) 1997-2005 The ClanLib Team
**
**  This software is provided 'as-is', without any express or implied
**  warranty.  In no event will the authors be held liable for any damages
**  arising from the use of this software.
**
**  Permission is granted to anyone to use this software for any purpose,
**  including commercial applications, and to alter it and redistribute it
**  freely, subject to the following restrictions:
**
**  1. The origin of this software must not be misrepresented; you must not
**     claim that you wrote the original software. If you use this software
**     in a product, an acknowledgment in the product documentation would be
**     appreciated but is not required.
**  2. Altered source versions must be plainly marked as such, and must not be
**     misrepresented as being the original software.
**  3. This notice may not be removed or altered from any source distribution.
**
**  Note: Some of the libraries ClanLib may link to may have additional
**  requirements or restrictions.
**
**  File Author(s):
**
**    Magnus Norddahl
**    (if your name is missing here, please add it)
*/

#include "util/command_line_generic.hpp"

#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <stdio.h>

#include "util/raise_exception.hpp"

#ifdef WIN32
#define snprintf _snprintf
#endif

CommandLine_Generic::CommandLine_Generic() :
  help_indent(18),
  programm(),
  options(),
  parsed_options(),
  current_option()
{
}

void CommandLine_Generic::parse_args(int argc, char** argv)
{
  programm = argv[0];

  for(int i = 1; i < argc; ++i)
  {
    if (argv[i][0] == '-')
    {
      if (argv[i][1] == '-')
      {
        // We got a long option
        if (argv[i][2] == '\0') {
          // Got a '--', so we stop evaluating arguments
          ++i;
          while(i < argc)
          {
            read_option(CommandLine::REST_ARG, argv[i]);
            ++i;
          }
        }
        else
        {
          std::string opt = argv[i] + 2;
          std::string long_opt;
          std::string long_opt_arg;

          std::string::size_type pos = opt.find('=');

          if (pos != std::string::npos)
          {
            long_opt = opt.substr(0, pos);
            long_opt_arg = opt.substr(pos+1);
          }
          else
          {
            long_opt = opt;
          }

          // Long Option
          Option* option = lookup_long_option(long_opt);

          if (option)
          {
            if (option->argument.empty())
            {
              read_option(option->key, "");
            }
            else
            {
              if (pos != std::string::npos)
              {
                read_option(option->key, long_opt_arg);
              }
              else
              {
                if (i == argc - 1)
                {
                  raise_exception(std::runtime_error,
                                  "option '" << std::string(argv[i]) << "' requires an argument");
                }
                else
                {
                  read_option(option->key, argv[i + 1]);
                  ++i;
                }
              }
            }
          }
          else
          {
            raise_exception(std::runtime_error, "unrecognized option '" << std::string(argv[i]) << "'");
          }
        }
      }
      else
      {
        // We got a short option
        char* p = argv[i] + 1;

        if (*p != '\0') {
          // Handle option chains
          while (*p)
          {
            // Short option(s)
            Option* option = lookup_short_option(*p);

            if (option)
            {
              if (option->argument.empty())
              {
                read_option(option->key, "");
              }
              else
              {
                if (i == argc - 1 || *(p+1) != '\0')
                {
                  // No more arguments
                  raise_exception(std::runtime_error, "option requires an argument -- " << std::string(1, *p));
                }
                else
                {
                  read_option(option->key, argv[i + 1]);
                  ++i;
                }
              }
            }
            else
            {
              raise_exception(std::runtime_error, "invalid option -- " << std::string(1, *p));
            }
            ++p;
          }
        }
        else
        {
          read_option(CommandLine::REST_ARG, "-");
        }
      }
    }
    else
    {
      read_option(CommandLine::REST_ARG, argv[i]);
    }
  }

  current_option = parsed_options.end();
}

CommandLine_Generic::Option *CommandLine_Generic::lookup_short_option(char short_option)
{
  for(Options::iterator i = options.begin(); i != options.end(); ++i)
  {
    if (i->key == short_option)
      return &(*i);
  }
  return 0;
}

CommandLine_Generic::Option *CommandLine_Generic::lookup_long_option(const std::string& long_option)
{
  for(Options::iterator i = options.begin(); i != options.end(); ++i)
  {
    if (i->long_option == long_option)
      return &*i;
  }
  return 0;
}

void CommandLine_Generic::read_option(int key, const std::string& argument)
{
  ParsedOption parsed_option;

  parsed_option.key = key;
  parsed_option.argument = argument;

  parsed_options.push_back(parsed_option);
}

void CommandLine_Generic::print_help()
{
  bool first_usage = true;
  for(Options::iterator i = options.begin(); i != options.end(); ++i)
  {
    if (i->visible)
    {
      if (i->key == USAGE)
      {
        if (first_usage)
        {
          std::cout << "Usage: " << programm << " " <<  i->help << std::endl;
          first_usage = false;
        }
        else
        {
          std::cout << "or:    " << programm << " " << i->help << std::endl;
        }
      }
      else if (i->key == GROUP)
      {
        if (i != options.begin())
          std::cout << std::endl;
        std::cout << i->help << std::endl;
      }
      else if (i->key == DOC)
      {
        if (i != options.begin())
          std::cout << std::endl;
        std::cout << i->help << std::endl;
      }
      else
      {
        char option[256]   = { 0 };
        char argument[256] = { 0 };

        if (i->key > 255 || i->key < 0)
          snprintf(option, 256, "--%s", i->long_option.c_str());
        else if (i->long_option.empty())
          snprintf(option, 256, "-%c", char(i->key));
        else
          snprintf(option, 256, "-%c, --%s", char(i->key), i->long_option.c_str());

        if (!i->argument.empty())
        {
          if (i->long_option.empty())
            snprintf(argument, 256, " %s", i->argument.c_str());
          else
            snprintf(argument, 256, "=%s", i->argument.c_str());
        }

        std::cout << "  "
                  << std::setiosflags(std::ios::left) << std::setw(help_indent)
                  << (std::string(option) + std::string(argument)) << std::setw(0)
                  << " " << i->help << std::endl;
      }
    }
  }
  std::cout << std::endl;
}

void CommandLine_Generic::add_usage(const std::string& usage)
{
  Option option;

  option.key          = USAGE;
  option.help         = usage;
  option.visible      = true;

  options.push_back(option);
}

void CommandLine_Generic::add_doc(const std::string& grouptopic)
{
  Option option;

  option.key          = DOC;
  option.help         = grouptopic;
  option.visible      = true;

  options.push_back(option);
}

void CommandLine_Generic::add_group(const std::string& grouptopic)
{
  Option option;

  option.key          = GROUP;
  option.help         = grouptopic;
  option.visible      = true;

  options.push_back(option);
}

void CommandLine_Generic::add_option(int key,
                                     const std::string& long_option,
                                     const std::string& argument,
                                     const std::string& help,
                                     bool visible)
{
  Option option;

  option.key          = key;
  option.help         = help;
  option.long_option  = long_option;
  option.argument     = argument;
  option.visible      = visible;

  options.push_back(option);
}

bool CommandLine_Generic::next()
{
  if (current_option == parsed_options.end())
  {
    (current_option = parsed_options.begin());
    return current_option != parsed_options.end();
  }
  else
  {
    return (++current_option) != parsed_options.end();
  }
}

int CommandLine_Generic::get_key()
{
  return current_option->key;
}

std::string CommandLine_Generic::get_argument()
{
  return current_option->argument;
}
